/**
 * 
 */
package com.xbang.core.web.interceptor;

import java.beans.PropertyDescriptor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
import com.opensymphony.xwork2.interceptor.Interceptor;
import com.xbang.core.BusinessException;

/**
 * @author dev.zhang
 */
public class ExceptionCatcherInterceptor implements Interceptor {

	private static final long serialVersionUID = 3925435415027955299L;

	private static final Log logger = LogFactory.getLog(ExceptionCatcherInterceptor.class);

	private List<String> ignoreProperties = Arrays.asList(new String[] { "cause", "class", "localizedMessage", "stackTrace" });
	
	private static final String EXCEPTION = "exception";

	private static final String ERROR = "error";

	private static final String DATABASE_ERROR = "database_error";

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.opensymphony.xwork2.interceptor.Interceptor#destroy()
	 */
	public void destroy() {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.opensymphony.xwork2.interceptor.Interceptor#init()
	 */
	public void init() {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.opensymphony.xwork2.interceptor.Interceptor#intercept(com.opensymphony.xwork2.ActionInvocation)
	 */
	public String intercept(ActionInvocation invocation) throws Exception {
		String result = null;
		try {
			result = invocation.invoke();
		} catch (BusinessException e) {
			// work with BusinessException, which is a checked exception
			logger.error("errorMessage:" + e.getErrorMessage());
			
			Map<String, Object> props = extractExceptionProperties(e);
			invocation.getStack().setValue("errorStack", props);
			invocation.getStack().setValue("errorMessage", props.get("errorMessage"));
			
			return e.getTarget() != null ? e.getTarget() : EXCEPTION;
			
		} catch (DataAccessException e) {
			// work with BusinessException, which is a checked exception
			logger.error("errorMessage:" + e.getMessage());
			return DATABASE_ERROR;
		} catch (RuntimeException e) {
			// work with RuntimeException, which is not a checked exception
			// but it is still a Runtime Exception
			logger.error(getExceptionMessage("RuntimeException", invocation), e);
			e.printStackTrace();
			return ERROR;
		} catch (Exception e) {
			// work with Exception, which looks like a system error
			logger.error(getExceptionMessage("Exception", invocation), e);
			e.printStackTrace();

			List<ExceptionMappingConfig> exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings();
			String mappedResult = this.findResultFromExceptions(exceptionMappings, e);
			if (mappedResult != null) {
				result = mappedResult;
			} else {
				return ERROR;
			}
		} catch (Error e) {
			logger.error(getExceptionMessage("Error", invocation), e);
			e.printStackTrace();
			return ERROR;
		} catch (Throwable e) {
			logger.error(getExceptionMessage("Throwable", invocation), e);
			e.printStackTrace();
			return ERROR;
		}
		return result;
	}

	/**
	 * 
	 * @param exceptionMappings
	 * @param t
	 * @return
	 */
	private String findResultFromExceptions(List<ExceptionMappingConfig> exceptionMappings, Throwable t) {
		String result = null;

		// Check for specific exception mappings.
		if (exceptionMappings != null) {
			int deepest = Integer.MAX_VALUE;
			for (Iterator<ExceptionMappingConfig> iter = exceptionMappings.iterator(); iter.hasNext();) {
				ExceptionMappingConfig exceptionMappingConfig = iter.next();
				int depth = getDepth(exceptionMappingConfig.getExceptionClassName(), t);
				if (depth >= 0 && depth < deepest) {
					deepest = depth;
					result = exceptionMappingConfig.getResult();
				}
			}
		}

		return result;
	}

	/**
	 * Return the depth to the superclass matching. 0 means ex matches exactly. Returns -1 if there's no match. Otherwise, returns depth. Lowest depth wins.
	 * 
	 * @param exceptionMapping
	 *            the mapping classname
	 * @param t
	 *            the cause
	 * @return the depth, if not found -1 is returned.
	 */
	public int getDepth(String exceptionMapping, Throwable t) {
		return getDepth(exceptionMapping, t.getClass(), 0);
	}

	/**
	 * 
	 * @param exceptionMapping
	 * @param exceptionClass
	 * @param depth
	 * @return
	 */
	private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) {
		if (exceptionClass.getName().indexOf(exceptionMapping) != -1) {
			// Found it!
			return depth;
		}
		// If we've gone as far as we can go and haven't found it...
		if (exceptionClass.equals(Throwable.class)) {
			return -1;
		}
		return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
	}

	/**
	 * 
	 * @param exception
	 * @param invocation
	 * @return
	 */
	private String getExceptionMessage(String exception, ActionInvocation invocation) {
		return "Caught" + exception + "while invoking action: " + invocation.getAction();
	}

	/**
	 * @param businessException
	 * @return
	 */
	private Map<String, Object> extractExceptionProperties(BusinessException businessException) {
		Map<String, Object> properties = new HashMap<String, Object>();
		// extract all the property in the BusinessException
		PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(businessException);
		for (int i = 0; i < descriptors.length; i++) {
			PropertyDescriptor descriptor = descriptors[i];
			// ignore system properties and those do not have read method
			if (descriptor.getReadMethod() == null || ignoreProperties.contains(descriptor.getName())) {
				continue;
			}
			try {
				properties.put(descriptor.getName(), PropertyUtils.getProperty(businessException, descriptor.getName()));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return properties;
	}
	
}
